package com.phonemap.math;

/*
 *    Copyright 2010 Tyler Coles
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.Serializable;
import java.util.Random;


/**
 * <p>A representation of a single point in latitude and longitude.
 * All data is handled in degrees and will be normalized if possible 
 * to the +/- 90 latitude, +/- 180 longitude region.</p>
 * 
 * <p>Note that attempting to create a LatLng with invalid values 
 * (NaN, negative infinity, positive infinity) will throw
 * IllegalArgumentExceptions.</p>
 * 
 * @author Tyler Coles
 */
public class LatLng implements Serializable {

        private static final long serialVersionUID = 7086953744720769418L;

        /**
         * Creates a random latitude and longitude. (Not inclusive of (-90, 0))
         */
        public static LatLng random() {
                return random(new Random());
        }

        /**
         * Creates a random latitude and longitude. (Not inclusive of (-90, 0))
         * 
         * @param r the random number generator to use, if you want to be 
         * specific or are creating many LatLngs at once.
         */
        public static LatLng random(Random r) {
                return new LatLng((r.nextDouble() * -180.0) + 90.0,
                                (r.nextDouble() * -360.0) + 180.0);
        }

        /**
         * Tests whether two angles fall within the tolerance
         * allowed in {@link com.javadocmd.simplelatlng.util.LatLngConfig}. Ignores
         * NaN and infinite values, returning false in either case.
         * 
         * @param degree1 one degree angle.
         * @param degree2 another degree angle.
         * @return true if they should be considered equal, false otherwise.
         */
        public static boolean degreesEqual(double degree1, double degree2) {
                if (Double.isNaN(degree1) || Double.isNaN(degree2))
                        return false;
                if (Double.isInfinite(degree1) || Double.isInfinite(degree2))
                        return false;
                return LatLngConfig.doubleToLong(degree1) == LatLngConfig
                                .doubleToLong(degree2);
        }

        private long latitude;
        private long longitude;

        /**
         * Creates a LatLng point.
         * 
         * @param latitude the latitude in degrees.
         * @param longitude the longitude in degrees.
         */
        public LatLng(double latitude, double longitude) {
                this.setLatitudeLongitude(latitude, longitude);
        }

        /**
         * Get latitude for this point in degrees.
         * 
         * @return latitude in degrees.
         */
        public double getLatitude() {
                return LatLngConfig.longToDouble(latitude);
        }

        /**
         * Get the internal long representation of this point's latitude
         * in degrees. Intended for library use only.
         * 
         * @return the internal representation of latitude in degrees.
         */
        public long getLatitudeInternal() {
                return latitude;
        }

        /**
         * Get longitude for this point in degrees.
         * 
         * @return longitude in degrees.
         */
        public double getLongitude() {
                return LatLngConfig.longToDouble(longitude);
        }

        /**
         * Get the internal long representation of this point's longitude
         * in degrees. Intended for library use only.
         * 
         * @return the internal representation of longitude in degrees.
         */
        public long getLongitudeInternal() {
                return longitude;
        }

        /**
         * Sets the latitude and longitude for this point.
         * 
         * @param latitude the latitude in degrees.
         * @param longitude the longitude in degrees.
         */
        public void setLatitudeLongitude(double latitude, double longitude) {
                this.setLatitude(latitude);
                if (Math.abs(this.latitude) == 90000000L) {
                        // At the poles all longitudes intersect. Simplify for later comparison.
                        this.setLongitude(0);
                } else {
                        this.setLongitude(longitude);
                }
        }

        /**
         * Sets the latitude for this point.
         */
        private void setLatitude(double latitude) {
                double lat = LatLngTool.normalizeLatitude(latitude);
                if (Double.isNaN(lat))
                        throw new IllegalArgumentException("Invalid latitude given.");
                this.latitude = LatLngConfig.doubleToLong(lat);
        }

        /**
         * Sets the longitude for this point.
         */
        private void setLongitude(double longitude) {
                double lng = LatLngTool.normalizeLongitude(longitude);
                if (Double.isNaN(lng))
                        throw new IllegalArgumentException("Invalid longitude given.");
                this.longitude = LatLngConfig.doubleToLong(lng);
        }

        /**
         * Returns true if this LatLng represents a polar coordinate (+/- 90 degrees latitude).
         */
        public boolean isPolar() {
                return this.latitude == 90000000L || this.latitude == -90000000L;
        }
        
        @Override
        public boolean equals(Object obj) {
                if (obj == this)
                        return true;
                if (!(obj instanceof LatLng))
                        return false;

                LatLng latlng = (LatLng) obj;
                if (this.latitude != latlng.latitude) {
                        return false;
                }

                return this.longitude == latlng.longitude;
        }

        @Override
        public int hashCode() {
                String s = Long.toString(latitude) + "|" + Long.toString(longitude);
                return s.hashCode();
        }

        @Override
        public String toString() {
                return String.format("(%s,%s)",
                                LatLngConfig.getDegreeFormat().format(LatLngConfig.longToDouble(this.latitude)),
                                LatLngConfig.getDegreeFormat().format(LatLngConfig.longToDouble(this.longitude)));
        }
}
